﻿using System;
using System.Collections.Generic;
using System.Text;
using ClientNodeLib.ServerNodeLib;

namespace ClientNodeLib
{
	/// <summary>
	/// Implements the scheduler client functionality.
	/// 
	/// This code needs a user interface, however it is written to be completely
	/// separate from such an interface.
	/// </summary>
	public class SimpleSchedulerClient : IDisposable
	{
		/// <summary>
		/// The WCF connection client.  Used as a global variable, basically.
		/// Please note you have to build the client!  Don't instantiate
		/// </summary>
		private SchedulerServerClient schedulerServerClient;
		/// <summary>
		/// The thread that refreshes state and updates the server
		/// </summary>
		private System.Threading.Thread refreshThread;
		/// <summary>
		/// A copy of this client's current status
		/// </summary>
		private ServerNodeLib.NodeStatus myStatus;
		/// <summary>
		/// The last time the client successfully reported to the server its status
		/// </summary>
		private DateTime lastReportTime;
		/// <summary>
		/// The queue of outgoing messages to the server.
		/// </summary>
		private ResultCommunicationDispatcher communicationQueue;
		/// <summary>
		/// The currently executing job or null if client is idle
		/// Long-term we could change this to a list of jobs...
		/// </summary>
		private BatchJob job;

		public SimpleSchedulerClient()
		{
			refreshThread = new System.Threading.Thread(this.RefreshLoop);
			myStatus = new ServerNodeLib.NodeStatus();
			myStatus.State = ServerNodeLib.NodeState.Idle;
			communicationQueue = new ResultCommunicationDispatcher();
			ClientNodeSettings.Default.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Default_PropertyChanged);
		}

		public void Start()
		{
			CommonLib.Tracer.Start();
			CommonLib.Tracer.StartActivity("Simple Client");
			refreshThread.Start();
			communicationQueue.Start();
			LoadStatus();
		}

		public void Stop()
		{
			if (refreshThread != null)
				refreshThread.Abort();
			communicationQueue.Stop();
			if (job != null)
				job.Stop();

			SaveStatus();
			CommonLib.Tracer.EndActivity("Simple Client");
			CommonLib.Tracer.Stop();
		}

		public void LoadStatus()
		{
			CommonLib.Tracer.StartActivity("Load Status");
			System.Xml.Serialization.XmlSerializer serializer;
			System.IO.StreamReader txtreader = null;
			try
			{
				serializer  = new System.Xml.Serialization.XmlSerializer(typeof(NodeStatus));
				txtreader = new System.IO.StreamReader("status.xml");
				myStatus = (NodeStatus) serializer.Deserialize(txtreader);
				CommonLib.Tracer.ReportInformation("Status loaded successfully");
			}
			catch (Exception ex)
			{
				CommonLib.Tracer.ReportWarning("Failed loading status", ex);
			}
			finally
			{
				serializer = null;
				if(txtreader != null)
					txtreader.Close();
				CommonLib.Tracer.EndActivity("Load Status");
			}
		}

		public void SaveStatus()
		{
			CommonLib.Tracer.StartActivity("Save Status");
			System.Xml.Serialization.XmlSerializer serializer;
			System.IO.StreamWriter txtreader = null;
			try
			{
				serializer = new System.Xml.Serialization.XmlSerializer(typeof(NodeStatus));
				txtreader = new System.IO.StreamWriter("status.xml");
				serializer.Serialize(txtreader, myStatus);
				CommonLib.Tracer.ReportInformation("Saved status succesfully");
			}
			catch (Exception ex)
			{
				CommonLib.Tracer.ReportWarning("Failed saving status", ex);
			}
			finally
			{
				serializer = null;
				if(txtreader != null)
					txtreader.Close();
				CommonLib.Tracer.EndActivity("Save Status");
			}
		}

		#region refresh operations
		/// <summary>
		/// Refreshes the state and updates the server of the client's status
		/// </summary>
		protected void RefreshLoop()
		{
			TimeSpan sleepTime = TimeSpan.FromSeconds(1);
			while (true)
			{
				try
				{
					System.Threading.Thread.Sleep(sleepTime);
					CommonLib.Tracer.StartActivity("Refresh Loop");

					schedulerServerClient = BuildWcfClient();
					if (schedulerServerClient == null)
						throw new System.ServiceModel.CommunicationException("Error connecting to server: check address");
					
					ServerNodeLib.ActionRequest action = UpdateStatus();
					if (action.ActionType == ServerNodeLib.ActionRequestType.AnnounceNode)
					{
						Announce();
					}
					else if (action.ActionType == ServerNodeLib.ActionRequestType.GetJob)
					{
						GetNextJob();
					}
					else if (action.ActionType == ServerNodeLib.ActionRequestType.KillJob)
					{
						KillJob();
					}
					else if (action.ActionType == ServerNodeLib.ActionRequestType.ChangeSetting)
					{
						ChangeSetting(action.PayloadKey, action.PayloadValue);
					}
					
					else if (action.ActionType == ServerNodeLib.ActionRequestType.ResetResultQueue)
					{
						QueueReset();
					}
					else if (action.ActionType == ServerNodeLib.ActionRequestType.SendResultQueue)
					{
						SendQueueResult(action.PayloadValue);
					}
					else if (action.ActionType == ServerNodeLib.ActionRequestType.ResultAccpeted)
					{
						AccpetQueueResult(action.PayloadValue);
					}
					
					// TODO: only update changed status items
					this.OnNodeDescriptionChange.Invoke(myStatus.StatusDescription);
					this.OnNodeIpChange.Invoke(myStatus.IpAddress);
					this.OnNodeLastStatusReportChange(DateTime.Now.ToString());
					this.OnNodeNameChange.Invoke(myStatus.NodeName);
					this.OnNodeStateChange.Invoke(myStatus.State.ToString() + " / " +  action.ActionType.ToString());
					this.OnNodeAcceptJobChange.Invoke(ClientNodeSettings.Default.AcceptNewJobs.ToString());
					this.OnNodeServerAddressChange(ClientNodeSettings.Default.ServerAddress);
					this.lastReportTime = DateTime.Now;
					SaveStatus();
					sleepTime = ClientNodeSettings.Default.LongTimeout;
				}
				catch (System.Threading.ThreadAbortException)
				{
					return;
				}
				catch (System.ServiceModel.CommunicationException ex)
				{
					OnNodeStateChange("Update Comm Exception (Timeout): " + ex.Message);
					sleepTime= ClientNodeSettings.Default.LongTimeout;
					CommonLib.Tracer.ReportError("Communication Exception", ex);
				}
				catch (Exception ex)
				{
					OnNodeStateChange("Update Exception: " + ex.Message);
					sleepTime = ClientNodeSettings.Default.LongTimeout;
					CommonLib.Tracer.ReportError("General Exception during refresh", ex);
					//throw ex;
				}
				finally
				{
					if(schedulerServerClient != null && schedulerServerClient.State == System.ServiceModel.CommunicationState.Opened)
						schedulerServerClient.Close();
					CommonLib.Tracer.EndActivity("Refresh Loop");
				}
			}
		}

		private void AccpetQueueResult(string batchId)
		{
			CommonLib.Tracer.ReportInformation("Sever acknowledges receipt of result " + batchId);
			this.communicationQueue.AcceptMessage(new Guid(batchId));
		}

		private void SendQueueResult(String batchId)
		{
			CommonLib.Tracer.ReportInformation("Sever requests resending result " + batchId);
			this.communicationQueue.ResetItem(new Guid(batchId));
		}

		private void QueueReset()
		{
			CommonLib.Tracer.ReportInformation("Sever requests reset of queue");
			this.communicationQueue.ResetQueue();
		}
		/// <summary>
		/// Sends the actual update message to the server.
		/// 
		/// The server responds to every update message with an action request
		/// </summary>
		/// <returns>The action request from the server</returns>
		private ServerNodeLib.ActionRequest UpdateStatus()
		{
			lock (myStatus)
			{
				CommonLib.Tracer.StartActivity("Updating Status");
				myStatus.IpAddress = GetIPAddress();
				CommonLib.Tracer.ReportInformation("IP is " + myStatus.IpAddress);
				myStatus.NodeName = ClientNodeSettings.Default.NodeName;
				CommonLib.Tracer.ReportInformation("NodeName is " + myStatus.NodeName);
				myStatus.AcceptNewJobs = ClientNodeSettings.Default.AcceptNewJobs;
				CommonLib.Tracer.ReportInformation("Accept New Jobs " + myStatus.AcceptNewJobs);
				myStatus.ErrorTimeout = (int) ClientNodeSettings.Default.ShortTimeout.TotalSeconds;
				CommonLib.Tracer.ReportInformation("Error timeout is " + myStatus.ErrorTimeout);
				myStatus.NormalTimeout = (int) ClientNodeSettings.Default.LongTimeout.TotalSeconds;
				CommonLib.Tracer.ReportInformation("Normal timeout is " + myStatus.NormalTimeout);
				if (job != null)
				{
					lock (job)
					{
						myStatus.BatchId = job.BatchId;
						CommonLib.Tracer.ReportInformation("Batch ID is " + myStatus.BatchId);
					}
				}
				else
				{
					myStatus.BatchId = Guid.Empty;
				}
				ActionRequest result = schedulerServerClient.SetStatus(myStatus.NodeId, myStatus);
				CommonLib.Tracer.ReportInformation("Action result is " + result.ActionType.ToString());
				CommonLib.Tracer.ReportInformation("Payload key is " + result.PayloadKey);
				CommonLib.Tracer.ReportInformation("Payload value is " + result.PayloadValue);
				CommonLib.Tracer.EndActivity("Updating Status");
				return result;
			}
		}

		/// <summary>
		/// Sends the announce message to the server.
		/// The server assigns a Guid to the client
		/// </summary>
		public void Announce()
		{
			lock (myStatus)
			{
				CommonLib.Tracer.ReportInformation("Sending announce message to server");
				myStatus.NodeName = ClientNodeSettings.Default.NodeName;
				myStatus.NodeId = schedulerServerClient.AnnounceMyself(myStatus.NodeName);
			}
		}
		/// <summary>
		/// Returns the current IP Address of the client.
		/// 
		/// Since this is self-reported, it may be incorrect if behind NATs, etc.
		/// </summary>
		/// <returns>The current IP Address</returns>
		public static String GetIPAddress()
		{
			// if we're behind NAT, we likely cannot get remote connections anyway
			// so it's ok to self-report ip
			System.Net.IPAddress[] ips = System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName());
			if (ips.Length <= 0)
				return string.Empty;
			else
				return ips[ips.Length - 1].ToString();
		}
		/// <summary>
		/// Provides hook for the GUI to kill the active job
		/// Also used when the server requests the job die
		/// </summary>
		public void KillJob()
		{
			CommonLib.Tracer.ReportInformation("Killing current job");
			if (job != null)
				job.Stop();
		}
		/// <summary>
		/// 
		/// </summary>
		public void GetNextJob()
		{
			CommonLib.Tracer.StartActivity("Get Next Job");
			if (job != null && job.IsRunning())
			{
				CommonLib.Tracer.ReportWarning("A job is already running.  Server must request a kill before asking us to download a new job");
				//throw new InvalidOperationException("A job is already running.  Server must request a kill before asking us to download a new job");
				//CommonLib.Logger.Report(CommonLib.LogType.Error, this, "A job is already running.  Server must request a kill before asking us to download a new job");
				return;
			}
			if (ClientNodeSettings.Default.AcceptNewJobs == false)
				return;
			// assert schedulerServerClient is not null
			ServerNodeLib.BatchInfo bi = schedulerServerClient.GetAvailableBatchJob(myStatus.NodeId);
			CommonLib.Tracer.ReportInformation("Next batch to get is " + bi.BatchId);
			job = new BatchJob(bi);
			lock (job)
			{
				job.OnBatchJobComplete = new BatchJobCompleteEvent(this.SendResult);
				job.OnBatchJobStart = new BatchJobStartEvent(this.HandleBatchStart);
				job.OnBatchJobCommandStep = new BatchJobCommandStep(this.HandleCommandStep);
				job.OnBatchJobDownloadResource = new BatchJobDownloadResource(this.HandleResourceDownload);
				job.Start();
			}
			lock (myStatus)
			{
				myStatus.StartTime = DateTime.Now;
			}
			CommonLib.Tracer.EndActivity("Get Next Job");
		}
		/// <summary>
		/// Handles the request to change a local setting.
		/// The server may only change a local setting IFF the client is
		/// configured to accept server control.
		/// 
		/// If so, we use reflection to find the particular setting from the
		/// setting object.  We then deserialize the value string and assign
		/// the setting.
		/// 
		/// I chose reflection so I wouldn't have to hard-code anything.
		/// </summary>
		/// <param name="key">The setting key or name</param>
		/// <param name="value">The string-serialization of the new value</param>
		public void ChangeSetting(String key, String value)
		{
			CommonLib.Tracer.StartActivity("Change Setting");
			try
			{
				CommonLib.Tracer.ReportInformation("Attempting to change setting: key = " + key + "; value = " + value);
				if (ClientNodeSettings.Default.AllowServerControl == false)
				{
					CommonLib.Tracer.ReportWarning("Attempted to change setting, but server control is disabled");
					return;
				}
				Type t = ClientNodeSettings.Default.GetType();
				foreach (System.Reflection.MethodInfo fi in t.GetMethods())
				{
					if (fi.Name.Equals("set_" + key))
					{
						Object o = null;
						Type vType = fi.GetParameters()[0].ParameterType;
						if (vType == typeof(String))
						{
							o = value;
						}
						else if (vType == typeof(int))
						{
							int i = 0;
							if (Int32.TryParse(value, out i))
							{
								o = i;
							}
						}
						else if (vType == typeof(double))
						{
							double i = 0;
							if (Double.TryParse(value, out i))
							{
								o = i;
							}
						}
						else if (vType == typeof(bool))
						{
							bool i = false;
							if (bool.TryParse(value, out i))
							{
								o = i;
							}
						}
						else if (vType == typeof(TimeSpan))
						{
							int i = 0;
							if (Int32.TryParse(value, out i))
							{
								if (i < 1)
									i = 1;
								o = i;
								o = TimeSpan.FromSeconds(i);
							}
						}

						if (o != null)
							fi.Invoke(ClientNodeSettings.Default, new object[] { o });
						return;
					}
				}
			}
			finally
			{
				CommonLib.Tracer.EndActivity("Change Setting");
			}
		}
		#endregion

		#region batch operations
		/// <summary>
		/// Sends the result of a batch completion to the server.
		/// 
		/// Please note that completion may be failure or error.
		/// </summary>
		/// <param name="info">The summary of the batch completion.</param>
		public void SendResult(ServerNodeLib.ResultInfo info)
		{
			if (info == null)
				return;

			CommonLib.Tracer.StartActivity("Send Result");

			List<ServerNodeLib.ResourceInfo> returnResources = new List<ServerNodeLib.ResourceInfo>();

			//only send files on a success
			if (info.IsSuccess)
			{
				CommonLib.Tracer.ReportInformation("Job was a success");
				lock (job)
				{
					foreach (ServerNodeLib.ResourceInfo resource in job.BatchInfo.ReturnResources)
					{
						returnResources.Add(resource);
					}
				}
			}
			else
			{
				CommonLib.Tracer.ReportInformation("Job was not a success");
			}
			communicationQueue.EnqueueMessage(info, returnResources);
			lock (job)
			{
				job = null;
			}
			lock (myStatus)
			{
				myStatus.EndTime = DateTime.Now;
				myStatus.StatusDescription = ClientNodeSettings.Default.BatchIdleFormat;
				myStatus.State = NodeState.Idle;
			}
			CommonLib.Tracer.EndActivity("Send Result");
		}
		/// <summary>
		/// Event handler for the job starting.
		/// Sets the status settings accordingly.
		/// 
		/// Long-term the BatchId is for multiple batches running on one client.
		/// </summary>
		/// <param name="batchId">The id of the batch.</param>
		public void HandleBatchStart(Guid batchId)
		{
			lock (myStatus)
			{
				myStatus.StartTime = DateTime.Now;
				myStatus.EndTime = DateTime.MaxValue;
				myStatus.StatusDescription = ClientNodeSettings.Default.BatchStartingFormat;
				myStatus.NodeName = ClientNodeSettings.Default.NodeName;
				myStatus.State = NodeState.Running;
			}
		}
		/// <summary>
		/// Event handler for the batch downlaoding a new resource
		/// </summary>
		/// <param name="batchId">The batch Id</param>
		/// <param name="path">The client path of the download</param>
		public void HandleResourceDownload(Guid batchId, String path)
		{
			lock (myStatus)
			{
				myStatus.State = ServerNodeLib.NodeState.DownloadingResources;
				myStatus.StatusDescription = String.Format(ClientNodeSettings.Default.BatchDownloadResourceFormat, System.IO.Path.GetFileName(path));
			}
		}
		/// <summary>
		/// Event handler for just before executing a command.
		/// </summary>
		/// <param name="batchId">The batch id</param>
		/// <param name="command">The plain-text command to execute</param>
		/// <param name="arguments">The plain-text arguments</param>
		public void HandleCommandStep(Guid batchId, String command, String arguments)
		{
			if (command == null)
				command = String.Empty;
			if (arguments == null)
				arguments = String.Empty;

			lock (myStatus)
			{
				myStatus.State = NodeState.Running;
				myStatus.StatusDescription = String.Format(ClientNodeSettings.Default.BatchRunningCommandFormat, command, arguments);
			}
		}
		#endregion

		#region options
		/// <summary>
		/// Returns the settings used by this object
		/// External code is free to get and set
		/// </summary>
		public ClientNodeSettings Settings
		{
			get
			{
				return ClientNodeSettings.Default;
			}
		}
		/// <summary>
		/// Event handler for a setting being changed.
		/// Allows us to ensure all setting changes are saved.
		/// Right now I don't have a reason to block changes -- I trust
		/// code to do the validation up front.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void Default_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			// make sure we always save the result
			ClientNodeSettings.Default.Save();
		}

		#endregion

		public UpdateStringEvent OnNodeNameChange;
		public UpdateStringEvent OnNodeIpChange;
		public UpdateStringEvent OnNodeStateChange;
		public UpdateStringEvent OnNodeDescriptionChange;
		public UpdateStringEvent OnNodeLastStatusReportChange;
		public UpdateStringEvent OnNodeAcceptJobChange;
		public UpdateStringEvent OnNodeServerAddressChange;

		#region IDisposable Members

		public void Dispose()
		{
			if (schedulerServerClient != null)
				schedulerServerClient.Close();
		}

		#endregion

		#region WCF Factory

		/// <summary>
		/// See the overloaded method for more information.
		/// 
		/// The client uses the saved settings for connection address.
		/// </summary>
		/// <returns></returns>
		public static SchedulerServerClient BuildWcfClient()
		{
			return BuildWcfClient(ClientNodeSettings.Default.ServerAddress);
		}

		/// <summary>
		/// You are responsible for disposing the client!!!
		/// This method is here to reduce code duplication.
		/// Consider it a fancy constructor, that's all
		/// </summary>
		/// <param name="address">The address to connect to.</param>
		/// <returns>Returns properly configured scheduler client that has been OPENED</returns>
		public static SchedulerServerClient BuildWcfClient(String address)
		{
			try
			{
				SchedulerServerClient s = new SchedulerServerClient("NetTcpBinding_ISchedulerServer", address);
				s.Open();
				return s;
			}
			catch (Exception ex)
			{
				CommonLib.Tracer.ReportError("Error connecting to server", ex);
				return null;
			}
		}

		#endregion
	}

	public delegate void UpdateStringEvent(String msg);
}
